package mn.more.mock.io.xml;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import mn.more.foundation.lang.StringUtil;
import mn.more.mock.io.MockDataReader;
import mn.more.mock.io.MockModel;
import org.jdom.Attribute;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * @author <a href="mailto:mike.liu@aptechmongolia.edu.mn">Mike Liu</a>
 * @version $Id: XMLReader.java 5 2008-09-01 12:08:42Z mikeliucc $
 */
public class XMLReader implements MockDataReader {

	private static final String DTD_IN_CLASSPATH = "/mn/more/mock/io/xml/model.dtd";
	private String filename;
	private Map<String, MockModel> parsedModels;
	private List<TempUnlinkModel> linkLater;
	private EntityResolver entityResolver = new EntityResolver() {

		public InputSource resolveEntity(String publicId, String systemId)
			throws SAXException, IOException {
			if (systemId != null && systemId.endsWith(XMLWriter.DTD_LOCATION)) {
				return new InputSource(getClass().getResourceAsStream(DTD_IN_CLASSPATH));
			} else {
				// I don't know you, so I'll leave you to your own demise
				return null;
			}
		}
	};
	private SAXBuilder saxBuilder = new SAXBuilder(true);

	public XMLReader() {
		// to resolve our own DTD (which can be different location depending on the host machine),
		// we add our own entity resolver here
		saxBuilder.setEntityResolver(entityResolver);
	}

	private static class TempUnlinkModel {

		private MockModel model;
		private String name;
		private String refId;

		public TempUnlinkModel(MockModel model, String name, String refId) {
			this.model = model;
			this.name = name;
			this.refId = refId;
		}

		public MockModel getModel() { return model; }

		public String getName() { return name; }

		public String getRefId() { return refId; }
	}

	public String getFilename() { return filename; }

	/** added to support spring's bean property setter, assume full path */
	public void setFile(File file) { this.filename = file.getAbsolutePath(); }

	public void setFilename(String filename, boolean classpath) throws IOException {
		if (classpath) {
			URL resource = getClass().getResource(filename);
			if (resource == null) {
				throw new FileNotFoundException("resource not found: " + filename);
			}
			this.filename = resource.getFile();
		} else {
			this.filename = filename;
		}

		this.filename = URLDecoder.decode(this.filename, "UTF-8");
		File f = new File(this.filename);
		if (!f.isFile() || !f.canRead()) {
			throw new FileNotFoundException("file not found: " + this.filename);
		}
	}

	/**
	 * This method first checks that the specified xml file exists, then parse the file to extract model information.
	 * <p/>
	 * However, model with reference to another model might throw a {@link NullPointerException} if the referenced model
	 * cannot be found by its id.
	 */
	public void read() throws IOException {
		Document doc;
		try {
			doc = saxBuilder.build(new File(filename));
		} catch (JDOMException e) {
			throw new IOException("Unable to property parse XML file found in '" + filename + "': " + e.getMessage());
		}

		parsedModels = new HashMap<String, MockModel>();
		linkLater = new ArrayList<TempUnlinkModel>();

		Element root = doc.getRootElement();
		List models = root.getChildren("model");
		for (Object child : models) {
			if (child instanceof Element) {
				Element elemModel = (Element) child;
				String id = elemModel.getAttribute("id").getValue();
				MockModel model = new MockModel(id);
				List properties = elemModel.getChildren();
				for (Object propertyObject : properties) {
					if (propertyObject instanceof Element) {
						Element elemProp = (Element) propertyObject;
						Attribute attrRefId = elemProp.getAttribute("refid");
						if (attrRefId != null) {
							linkReference(model, elemProp);
						} else {
							setProperty(model, elemProp);
						}
					}
				}

				parsedModels.put(model.getId(), model);
			}
		}

		for (TempUnlinkModel unlinkModel : linkLater) {
			String refId = unlinkModel.getRefId();
			String name = unlinkModel.getName();
			MockModel model = unlinkModel.getModel();
			if (parsedModels.containsKey(refId)) {
				model.setValue(name, parsedModels.get(refId));
			} else {
				throw new NullPointerException(
					"model (ID=" + model.getId() +
					") contains a property named '" + name +
					"' referencing another model (ID=" + refId +
					").  But the referenced model cannot be found.");
			}
		}
	}

	public MockModel getModel(String id) { return parsedModels.get(id); }

	public List<String> getIdList() {
		// use a new object so that the original keys will not be tempered with
		List<String> idList = new ArrayList<String>();
		idList.addAll(parsedModels.keySet());
		return idList;
	}

	private void linkReference(MockModel model, Element elemProp) {
		Attribute attrRefId = elemProp.getAttribute("refid");
		String refId = attrRefId.getValue();
		String name = elemProp.getAttribute("name").getValue();
		if (parsedModels.containsKey(refId)) {
			model.setValue(name, parsedModels.get(refId));
		} else {
			linkLater.add(new TempUnlinkModel(model, name, refId));
		}
	}

	private static void setProperty(MockModel model, Element elemProp) {
		String type = elemProp.getAttribute("type").getValue();
		String name = elemProp.getAttribute("name").getValue();
		String value = elemProp.getText();
		if (value == null) { return; }

		if ("boolean".equals(type)) { model.setValue(name, toBoolean(value)); }
		if ("byte".equals(type)) { model.setValue(name, toByte(value)); }
		if ("short".equals(type)) { model.setValue(name, toShort(value)); }
		if ("char".equals(type)) { model.setValue(name, toChar(value)); }
		if ("int".equals(type)) { model.setValue(name, toInt(value)); }
		if ("long".equals(type)) { model.setValue(name, toLong(value)); }
		if ("float".equals(type)) { model.setValue(name, toFloat(value)); }
		if ("double".equals(type)) { model.setValue(name, toDouble(value)); }
		if ("String".equals(type)) { model.setValue(name, replaceCharacters(value)); }

		if ("booleanArray".equals(type)) { model.setValue(name, toBooleanArray(value)); }
		if ("byteArray".equals(type)) { model.setValue(name, toByteArray(value)); }
		if ("shortArray".equals(type)) { model.setValue(name, toShortArray(value)); }
		if ("charArray".equals(type)) { model.setValue(name, toCharArray(value)); }
		if ("intArray".equals(type)) { model.setValue(name, toIntArray(value)); }
		if ("longArray".equals(type)) { model.setValue(name, toLongArray(value)); }
		if ("floatArray".equals(type)) { model.setValue(name, toFloatArray(value)); }
		if ("doubleArray".equals(type)) { model.setValue(name, toDoubleArray(value)); }
		if ("StringArray".equals(type)) { model.setValue(name, toArray(value)); }
	}

	private static String[] toArray(String value) {
		StringTokenizer tokens = new StringTokenizer(value.trim(), XMLWriter.DELIM);
		String[] array = new String[tokens.countTokens()];
		int i = 0;
		while (tokens.hasMoreTokens()) {
			array[i] = replaceCharacters(tokens.nextToken());
			i++;
		}
		return array;
	}

	private static boolean[] toBooleanArray(String value) {
		StringTokenizer tokens = new StringTokenizer(value.trim(), XMLWriter.DELIM);
		boolean[] array = new boolean[tokens.countTokens()];
		int i = 0;
		while (tokens.hasMoreTokens()) {
			array[i] = toBoolean(tokens.nextToken());
			i++;
		}
		return array;
	}

	private static byte[] toByteArray(String value) {
		StringTokenizer tokens = new StringTokenizer(value.trim(), XMLWriter.DELIM);
		byte[] array = new byte[tokens.countTokens()];
		int i = 0;
		while (tokens.hasMoreTokens()) {
			array[i] = toByte(tokens.nextToken());
			i++;
		}
		return array;
	}

	private static short[] toShortArray(String value) {
		StringTokenizer tokens = new StringTokenizer(value.trim(), XMLWriter.DELIM);
		short[] array = new short[tokens.countTokens()];
		int i = 0;
		while (tokens.hasMoreTokens()) {
			array[i] = toShort(tokens.nextToken());
			i++;
		}
		return array;
	}

	private static char[] toCharArray(String value) {
		StringTokenizer tokens = new StringTokenizer(value.trim(), XMLWriter.DELIM);
		char[] array = new char[tokens.countTokens()];
		int i = 0;
		while (tokens.hasMoreTokens()) {
			array[i] = toChar(tokens.nextToken());
			i++;
		}
		return array;
	}

	private static int[] toIntArray(String value) {
		StringTokenizer tokens = new StringTokenizer(value.trim(), XMLWriter.DELIM);
		int[] array = new int[tokens.countTokens()];
		int i = 0;
		while (tokens.hasMoreTokens()) {
			array[i] = toInt(tokens.nextToken());
			i++;
		}
		return array;
	}

	private static long[] toLongArray(String value) {
		StringTokenizer tokens = new StringTokenizer(value.trim(), XMLWriter.DELIM);
		long[] array = new long[tokens.countTokens()];
		int i = 0;
		while (tokens.hasMoreTokens()) {
			array[i] = toLong(tokens.nextToken());
			i++;
		}
		return array;
	}

	private static float[] toFloatArray(String value) {
		StringTokenizer tokens = new StringTokenizer(value.trim(), XMLWriter.DELIM);
		float[] array = new float[tokens.countTokens()];
		for (int i = 0; i < tokens.countTokens(); i++) { array[i] = toFloat(tokens.nextToken()); }
		return array;
	}

	private static double[] toDoubleArray(String value) {
		StringTokenizer tokens = new StringTokenizer(value.trim(), XMLWriter.DELIM);
		double[] array = new double[tokens.countTokens()];
		int i = 0;
		while (tokens.hasMoreTokens()) {
			array[i] = toDouble(tokens.nextToken());
			i++;
		}
		return array;
	}

	/** special character treatment to deal with characters not acceptable in XML format (well, at least not easily). */
	private static String replaceCharacters(String text) {
		text = StringUtil.replace(text, "&nbsp;", " ");

		int startPos = 0;
		while (startPos != -1) {
			startPos = text.indexOf("&#", startPos);
			if (startPos == -1) { return text; }

			int endPos = text.indexOf(";", startPos + 2);
			if (endPos == -1) { return text; }

			String asciiText = text.substring(startPos + 2, endPos);
			try {
				text = text.substring(0, startPos) + (char) Integer.parseInt(asciiText) + text.substring(endPos + 1);
			} catch (NumberFormatException e) {
				// not a number? let's ignore it!
				//continue;
			}
		}

		return text;
	}

	private static boolean toBoolean(String value) { return Boolean.parseBoolean(value); }

	private static byte toByte(String value) { return Byte.parseByte(value); }

	private static short toShort(String value) { return Short.parseShort(value); }

	private static char toChar(String value) { return value.charAt(0); }

	private static int toInt(String value) { return Integer.parseInt(value); }

	private static long toLong(String value) { return Long.parseLong(value); }

	private static float toFloat(String value) { return Float.parseFloat(value); }

	private static double toDouble(String value) { return Double.parseDouble(value); }

}
